/**********************************************************************

  Audacity: A Digital Audio Editor

  DirectoriesPrefs.cpp

  Joshua Haberman
  James Crook


*******************************************************************//**

\class DirectoriesPrefs
\brief A PrefsPanel used to select directories.

*//*******************************************************************/

#include "DirectoriesPrefs.h"

#include <math.h>

#include <wx/defs.h>
#include <wx/log.h>
#include <wx/stattext.h>
#include <wx/textctrl.h>
#include <wx/dirdlg.h>
#include <wx/filefn.h>
#include <wx/filename.h>

#include "Prefs.h"
#include "ShuttleGui.h"
#include "TempDirectory.h"
#include "AudacityMessageBox.h"
#include "ReadOnlyText.h"
#include "FileNames.h"

using namespace FileNames;
using namespace TempDirectory;

class FilesystemValidator : public wxValidator
{
public:
    FilesystemValidator(const TranslatableString& message)
        :  wxValidator()
    {
        mMessage = message;
    }

    virtual wxObject* Clone() const wxOVERRIDE
    {
        return safenew FilesystemValidator(mMessage);
    }

    virtual bool Validate(wxWindow* WXUNUSED(parent)) wxOVERRIDE
    {
        wxTextCtrl* tc = wxDynamicCast(GetWindow(), wxTextCtrl);
        if (!tc) {
            return true;
        }

        if (FATFilesystemDenied(tc->GetValue(), mMessage)) {
            return false;
        }

        return true;
    }

    virtual bool TransferToWindow() wxOVERRIDE
    {
        return true;
    }

    virtual bool TransferFromWindow() wxOVERRIDE
    {
        return true;
    }

    void OnChar(wxKeyEvent& evt)
    {
        evt.Skip();

        wxTextCtrl* tc = wxDynamicCast(GetWindow(), wxTextCtrl);
        if (!tc) {
            return;
        }

        auto keycode = evt.GetUnicodeKey();
        if (keycode < WXK_SPACE || keycode == WXK_DELETE) {
            return;
        }

        wxString path = tc->GetValue();
        path.insert(tc->GetInsertionPoint(), keycode);

        if (FATFilesystemDenied(path, mMessage)) {
            evt.Skip(false);
            return;
        }
    }

    TranslatableString mMessage;

    wxDECLARE_EVENT_TABLE();
};

wxBEGIN_EVENT_TABLE(FilesystemValidator, wxValidator)
EVT_CHAR(FilesystemValidator::OnChar)
wxEND_EVENT_TABLE()

enum
{
    TempTextID = 1000,
    TempButtonID,

    TextsStart = 1010,
    OpenTextID,
    SaveTextID,
    ImportTextID,
    ExportTextID,
    MacrosTextID,
    TextsEnd,

    ButtonsStart = 1020,
    OpenButtonID,
    SaveButtonID,
    ImportButtonID,
    ExportButtonID,
    MacrosButtonID,
    ButtonsEnd
};

BEGIN_EVENT_TABLE(DirectoriesPrefs, PrefsPanel)
EVT_TEXT(TempTextID, DirectoriesPrefs::OnTempText)
EVT_BUTTON(TempButtonID, DirectoriesPrefs::OnTempBrowse)
EVT_COMMAND_RANGE(ButtonsStart, ButtonsEnd, wxEVT_BUTTON, DirectoriesPrefs::OnBrowse)
END_EVENT_TABLE()

DirectoriesPrefs::DirectoriesPrefs(wxWindow* parent, wxWindowID winid)
/* i18n-hint:  Directories, also called directories, in computer file systems */
    :  PrefsPanel(parent, winid, XO("Directories")),
    mFreeSpace(NULL),
    mTempText(NULL)
{
    Populate();
}

DirectoriesPrefs::~DirectoriesPrefs()
{
}

ComponentInterfaceSymbol DirectoriesPrefs::GetSymbol() const
{
    return DIRECTORIES_PREFS_PLUGIN_SYMBOL;
}

TranslatableString DirectoriesPrefs::GetDescription() const
{
    return XO("Preferences for Directories");
}

ManualPageID DirectoriesPrefs::HelpPageName()
{
    return "Directories_Preferences";
}

/// Creates the dialog and its contents.
void DirectoriesPrefs::Populate()
{
    //------------------------- Main section --------------------
    // Now construct the GUI itself.
    // Use 'eIsCreatingFromPrefs' so that the GUI is
    // initialised with values from gPrefs.
    ShuttleGui S(this, eIsCreatingFromPrefs);
    PopulateOrExchange(S);
    // ----------------------- End of main section --------------

    wxCommandEvent e;
    OnTempText(e);
}

void DirectoriesPrefs::PopulateOrExchange(ShuttleGui& S)
{
    S.SetBorder(2);
    S.StartScroller();

    S.StartStatic(XO("Default directories"));
    {
        S.AddSpace(1);
        S.AddFixedText(XO("Leave a field empty to go to the last directory used for that operation.\n"
                          "Fill in a field to always go to that directory for that operation."), false, 450);
        S.AddSpace(5);

        S.StartMultiColumn(3, wxEXPAND);
        {
            S.SetStretchyCol(1);

            S.Id(OpenTextID);
            mOpenText = S.TieTextBox(XXO("O&pen:"),
                                     { PreferenceKey(Operation::Open, PathType::User),
                                       wxT("") },
                                     30);
            S.Id(OpenButtonID).AddButton(XXO("&Browse..."));

            S.Id(SaveTextID);
            mSaveText = S.TieTextBox(XXO("S&ave:"),
                                     { PreferenceKey(Operation::Save, PathType::User),
                                       wxT("") },
                                     30);
            if (mSaveText) {
                mSaveText->SetValidator(FilesystemValidator(XO("Projects cannot be saved to FAT drives.")));
            }
            S.Id(SaveButtonID).AddButton(XXO("B&rowse..."));

            S.Id(ImportTextID);
            mImportText = S.TieTextBox(XXO("&Import:"),
                                       { PreferenceKey(Operation::Import, PathType::User),
                                         wxT("") },
                                       30);
            S.Id(ImportButtonID).AddButton(XXO("Br&owse..."));

            S.Id(ExportTextID);
            mExportText = S.TieTextBox(XXO("&Export:"),
                                       { PreferenceKey(Operation::Export, PathType::User),
                                         wxT("") },
                                       30);
            S.Id(ExportButtonID).AddButton(XXO("Bro&wse..."));

            S.Id(MacrosTextID);
            mMacrosText = S.TieTextBox(XXO("&Macro output:"),
                                       { PreferenceKey(Operation::MacrosOut, PathType::User),
                                         wxT("") },
                                       30);
            S.Id(MacrosButtonID).AddButton(XXO("Bro&wse..."));
        }
        S.EndMultiColumn();
    }
    S.EndStatic();

    S.StartStatic(XO("Temporary files directory"));
    {
        S.StartMultiColumn(3, wxEXPAND);
        {
            S.SetStretchyCol(1);

            S.Id(TempTextID);
            mTempText = S.TieTextBox(XXO("&Location:"),
                                     { PreferenceKey(Operation::Temp, PathType::_None),
                                       wxT("") },
                                     30);
            if (mTempText) {
                mTempText->SetValidator(FilesystemValidator(XO("Temporary files directory cannot be on a FAT drive.")));
            }
            S.Id(TempButtonID).AddButton(XXO("Brow&se..."));

            mFreeSpace = S
                         .AddReadOnlyText(XXO("&Free Space:"), "");
        }
        S.EndMultiColumn();
    }
    S.EndStatic();

    S.EndScroller();
}

void DirectoriesPrefs::OnTempBrowse(wxCommandEvent& evt)
{
    wxString oldTemp = gPrefs->Read(PreferenceKey(Operation::Open, PathType::_None),
                                    DefaultTempDir());

    // Because we went through InitTemp() during initialisation,
    // the old temp directory name in prefs should already be OK.  Just in case there is
    // some way we hadn't thought of for it to be not OK,
    // we avoid prompting with it in that case and use the suggested default instead.
    if (!IsTempDirectoryNameOK(oldTemp)) {
        oldTemp = DefaultTempDir();
    }

    wxDirDialogWrapper dlog(this,
                            XO("Choose a location to place the temporary directory"),
                            oldTemp);
    int retval = dlog.ShowModal();
    if (retval != wxID_CANCEL && !dlog.GetPath().empty()) {
        wxFileName tmpDirPath;
        tmpDirPath.AssignDir(dlog.GetPath());

        if (FATFilesystemDenied(tmpDirPath.GetFullPath(),
                                XO("Temporary files directory cannot be on a FAT drive."))) {
            return;
        }

        //Checks if the temporary directory has write permissions(via Browse Button)
        if (!FileNames::WritableLocationCheck(dlog.GetPath(), XO("Cannot set the preference."))) {
            return;
        }

        // Append an "audacity_temp" directory to this path if necessary (the
        // default, the existing pref (as stored in the control), and any path
        // ending in a directory with the same name as what we'd add should be OK
        // already)
        wxString newDirName;
#if defined(__WXMAC__)
        newDirName = wxT("SessionData");
#elif defined(__WXMSW__)
        // Clearing Bug 1271 residual issue.  Let's NOT have temp in the name.
        newDirName = wxT("SessionData");
#else
        newDirName = wxT(".audacity_temp");
#endif
        auto dirsInPath = tmpDirPath.GetDirs();

        // If the default temp dir or user's pref dir don't end in '/' they cause
        // wxFileName's == operator to construct a wxFileName representing a file
        // (that doesn't exist) -- hence the constructor calls
        if (tmpDirPath != wxFileName(DefaultTempDir(), wxT(""))
            && tmpDirPath != wxFileName(mTempText->GetValue(), wxT(""))
            && (dirsInPath.size() == 0
                || dirsInPath[dirsInPath.size() - 1] != newDirName)) {
            tmpDirPath.AppendDir(newDirName);
        }

        mTempText->SetValue(tmpDirPath.GetPath(wxPATH_GET_VOLUME | wxPATH_GET_SEPARATOR));
        OnTempText(evt);
    }
}

void DirectoriesPrefs::OnTempText(wxCommandEvent& WXUNUSED(evt))
{
    TranslatableString label;

    if (mTempText && mFreeSpace) {
        FilePath path = mTempText->GetValue();

        wxLongLong space;
        wxGetDiskSpace(path, NULL, &space);

        label = wxDirExists(path)
                ? Internat::FormatSize(space)
                : XO("unavailable - above location doesn't exist");

        mFreeSpace->SetValue(label.Translation());
    }
}

void DirectoriesPrefs::OnBrowse(wxCommandEvent& evt)
{
    long id = evt.GetId() - ButtonsStart;
    wxTextCtrl* tc = (wxTextCtrl*)FindWindow(id + TextsStart);

    wxString location = tc->GetValue();

    wxDirDialogWrapper dlog(this,
                            XO("Choose a location"),
                            location);
    int retval = dlog.ShowModal();

    if (retval == wxID_CANCEL) {
        return;
    }

    if (evt.GetId() == SaveButtonID) {
        if (FATFilesystemDenied(dlog.GetPath(),
                                XO("Projects cannot be saved to FAT drives."))) {
            return;
        }
    }

    //Checks if the location for Open,Save.Import,Export and Macro Output has write permissions(Browse Buttons)
    if (!FileNames::WritableLocationCheck(dlog.GetPath(), XO("Cannot set the preference."))) {
        return;
    }

    tc->SetValue(dlog.GetPath());
}

// Offers the user a dialog with an option to create a directory if it does not exist.
// message is the explanation given to the user to show for which case is the directory creation is prompted.
bool CreateDirectory(const wxString pathString, const TranslatableString& message)
{
    const wxFileName path { pathString };
    int ans = AudacityMessageBox(
        message
        + XO("\nDirectory %s does not exist. Create it?")
        .Format(pathString),
        XO("Warning"),
        wxYES_NO | wxCENTRE | wxICON_EXCLAMATION);

    if (ans != wxYES) {
        return false;
    }

    if (!path.Mkdir(0755, wxPATH_MKDIR_FULL)) {
        /* wxWidgets throws up a decent looking dialog */
        using namespace BasicUI;
        ShowMessageBox(
            XO("Directory creation failed.")
            + XO("\n%s").Format(message),
            MessageBoxOptions {}
            .Caption(XO("Error"))
            .IconStyle(Icon::Error)
            .ButtonStyle(Button::Ok)
            );
        return false;
    }
    return true;
}

bool DirectoriesPrefs::Validate()
{
    wxFileName Temp;
    Temp.SetPath(mTempText->GetValue());

    wxString path{ Temp.GetPath() };
    if (!IsTempDirectoryNameOK(path)) {
        AudacityMessageBox(
            XO("Directory %s is not suitable (at risk of being cleaned out)")
            .Format(path),
            XO("Error"),
            wxOK | wxICON_ERROR);
        return false;
    }

    if (!Temp.DirExists()) {
        if (CreateDirectory(path, XO("'Temporary Directory' cannot be set.")) == false) {
            return false;
        }
    } else {
        /* If the directory already exists, make sure it is writable */
        if (!FileNames::WritableLocationCheck(mTempText->GetValue(),
                                              XO("'Temporary files' directory cannot be set."))) {
            return false;
        }
        wxLogNull logNo;
        Temp.AppendDir(wxT("canicreate"));
        path =  Temp.GetPath();
        if (!Temp.Mkdir(0755)) {
            AudacityMessageBox(
                XO("Directory %s is not writable")
                .Format(path),
                XO("Error"),
                wxOK | wxICON_ERROR);
            return false;
        }
        Temp.Rmdir();
        Temp.RemoveLastDir();
    }

    wxFileName oldDir;
    oldDir.SetPath(TempDir());
    if (Temp != oldDir) {
        AudacityMessageBox(
            XO(
                "Changes to temporary directory will not take effect until Audacity is restarted"),
            XO("Temp Directory Update"),
            wxOK | wxCENTRE | wxICON_INFORMATION);
    }

    const wxString openPathString = mOpenText->GetValue();
    const wxString savePathString = mSaveText->GetValue();
    const wxString importPathString = mImportText->GetValue();
    const wxString exportPathString = mExportText->GetValue();
    const wxString macroPathString = mMacrosText->GetValue();
    //error messages if the directories could not be set.
    const std::initializer_list<TranslatableString> messagesPreference{
        XO("'Open' directory cannot be set."),
        XO("'Save' directory cannot be set."),
        XO("'Import' directory cannot be set."),
        XO("'Export' directory cannot be set."),
        XO("'Macro Output' directory cannot be set."),
    };

    //flag for checking if at least one directory write protected
    //will not be 0 if any of the paths are not writable
    int flag = 0;
    //id for indexing error messages to the initializer_list.
    int id = 0;
    //Checks if the location for Open,Save,Import,Export and Macro Output has write permissions(When OK is clicked)
    for (auto& string : { openPathString, savePathString, importPathString, exportPathString, macroPathString } ) {
        const wxFileName currentPath { string };
        const auto& message = *(messagesPreference.begin() + id);
        if (!string.empty()) {
            if (currentPath.DirExists()) {
                if (!FileNames::WritableLocationCheck(string, message)) {
                    flag++;
                }
            } else {
                return CreateDirectory(string, message);
            }
        }
        id++;
    }
    if (flag != 0) {
        return false;
    }

    return true;
}

bool DirectoriesPrefs::Commit()
{
    ShuttleGui S(this, eIsSavingToPrefs);
    PopulateOrExchange(S);

    return true;
}

PrefsPanel::Factory
DirectoriesPrefsFactory()
{
    return [](wxWindow* parent, wxWindowID winid, AudacityProject*)
    {
        wxASSERT(parent); // to justify safenew
        return safenew DirectoriesPrefs(parent, winid);
    };
}

namespace {
PrefsPanel::Registration sAttachment
{
    "Directories",
    DirectoriesPrefsFactory()
};
}
